using System;
using System.Globalization;

namespace Measurements
{
    /// <summary>
    /// Options for temperature measurement units.
    /// </summary>
    public enum TemperatureUnit
    {
        /// <summary>
        /// The SI base unit of thermodynamic temperature, equal in magnitude to the degree Celsius.
        /// </summary>
        Kelvin,

        /// <summary>
        /// A scale of temperature on which water freezes at 0° and boils at 100° under standard conditions.
        /// </summary>
        Celsius,

        /// <summary>
        /// A scale of temperature on which water freezes at 32° and boils at 212° under standard conditions.
        /// </summary>
        Fahrenheit
    }

    /// <summary>
    /// A temperature value.
    /// </summary>
    public struct Temperature : IFormattable, IComparable, IComparable<Temperature>, IEquatable<Temperature>
    {
        private double _Kelvin;

        /// <summary>
        /// Creates a new temperature with the specified value in Kelvin.
        /// </summary>
        /// <param name="kelvin">The value of the temperature.</param>
        public Temperature(double kelvin) : this() { _Kelvin = kelvin; }

        /// <summary>
        /// Creates a new temperature with the specified value in the
        /// specified unit of measurement.
        /// </summary>
        /// <param name="temperature">The value of the temperature.</param>
        /// <param name="unit">The unit of measurement that defines how the <paramref name="temperature"/> value is used.</param>
        public Temperature(double temperature, TemperatureUnit unit)
            : this()
        {
            switch (unit)
            {
                case TemperatureUnit.Kelvin:
                    Kelvin = temperature;
                    break;
                case TemperatureUnit.Celsius:
                    Celsius = temperature;
                    break;
                case TemperatureUnit.Fahrenheit:
                    Fahrenheit = temperature;
                    break;
                default:
                    throw new ArgumentException("The temperature unit '" + unit.ToString() + "' is unknown.");
            }
        }

        /// <summary>
        /// Gets or sets the temperature value in Kelvin.
        /// </summary>
        public double Kelvin
        {
            get { return _Kelvin; }
            set { _Kelvin = value; }
        }

        /// <summary>
        /// Gets or sets the temperature value in Celsius.
        /// </summary>
        public double Celsius
        {
            get { return KelvinToCelsius(Kelvin); }
            set { Kelvin = CelsiusToKelvin(value); }
        }

        /// <summary>
        /// Gets or sets the temperature value in Fahrenheit.
        /// </summary>
        public double Fahrenheit
        {
            get { return KelvinToFahrenheit(Kelvin); }
            set { Kelvin = FahrenheitToKelvin(value); }
        }

        /// <summary>
        /// Gets the temperature value in the specified unit of measurement.
        /// </summary>
        /// <param name="unit">The unit of measurement in which the temperature should be returned.</param>
        /// <returns>The temperature value in the specified <paramref name="unit"/>.</returns>
        public double ValueIn(TemperatureUnit unit)
        {
            switch (unit)
            {
                case TemperatureUnit.Kelvin: return Kelvin;
                case TemperatureUnit.Celsius: return Celsius;
                case TemperatureUnit.Fahrenheit: return Fahrenheit;
                default: throw new ArgumentException("Unknown temperature unit '" + unit.ToString() + "'.");
            }
        }

        /// <summary>
        /// Returns a string representation of the temperature value.
        /// </summary>
        /// <param name="format">
        /// A single format specifier that indicates how to format the value of this
        /// temperature. The format parameter can be "G", "C", "F", or "K". If format
        /// is null or the empty string (""), "G" is used.
        /// </param>
        /// <param name="provider">
        /// An IFormatProvider reference that supplies culture-specific formatting
        /// services.
        /// </param>
        /// <returns>A string representation of the temperature.</returns>
        /// <exception cref="FormatException">
        /// The value of format is not null, the empty string (""), "G", "C", "F", or
        /// "K".
        /// </exception>
        public string ToString(string format, IFormatProvider provider)
        {
            if (String.IsNullOrEmpty(format)) format = "G";
            if (provider == null) provider = CultureInfo.CurrentCulture;

            switch (format.ToUpperInvariant())
            {
                case "G":
                case "C":
                    return Celsius.ToString("F2", provider) + " °C";
                case "F":
                    return Fahrenheit.ToString("F2", provider) + " °F";
                case "K":
                    return Kelvin.ToString("F2", provider) + " K";
                default:
                    throw new FormatException(String.Format("The {0} format string is not supported.", format));
            }
        }

        /// <summary>
        /// Returns a string representation of the temperature value.
        /// </summary>
        /// <param name="format">
        /// A single format specifier that indicates how to format the value of this
        /// temperature. The format parameter can be "G", "C", "F", or "K". If format
        /// is null or the empty string (""), "G" is used.
        /// </param>
        /// <returns>A string representation of the temperature.</returns>
        /// <exception cref="FormatException">
        /// The value of format is not null, the empty string (""), "G", "C", "F", or
        /// "K".
        /// </exception>
        public string ToString(string format)
        {
            return ToString(format, null);
        }

        /// <summary>
        /// Returns a string representation of the temperature value.
        /// </summary>
        /// <returns>A string representation of the temperature.</returns>
        public override string ToString()
        {
            return ToString(null, null);
        }

        /// <summary>
        /// Returns a string representation of the temperature value.
        /// </summary>
        /// <param name="unit">
        /// The temperature unit as which the temperature value should be displayed.
        /// </param>
        /// <param name="provider">
        /// An IFormatProvider reference that supplies culture-specific formatting
        /// services.
        /// </param>
        /// <returns>A string representation of the temperature.</returns>
        public string ToString(TemperatureUnit unit, IFormatProvider provider)
        {
            switch (unit)
            {
                case TemperatureUnit.Celsius:
                    return ToString("C", provider);
                case TemperatureUnit.Fahrenheit:
                    return ToString("F", provider);
                case TemperatureUnit.Kelvin:
                    return ToString("K", provider);
                default:
                    throw new FormatException("The temperature unit '" + unit.ToString() + "' is unknown.");
            }
        }

        /// <summary>
        /// Returns a string representation of the temperature value.
        /// </summary>
        /// <param name="unit">
        /// The temperature unit as which the temperature value should be displayed.
        /// </param>
        /// <returns>A string representation of the temperature.</returns>
        public string ToString(TemperatureUnit unit)
        {
            return ToString(unit, null);
        }

        /// <summary>
        /// Compares this instance to a specified Temperature object and returns an indication
        /// of their relative values.
        /// </summary>
        /// <param name="value">A Temperature object to compare to this instance.</param>
        /// <returns>
        /// A signed number indicating the relative values of this instance and value.
        /// Value Description A negative integer This instance is less than value. Zero
        /// This instance is equal to value. A positive integer This instance is greater
        /// than value.
        /// </returns>
        public int CompareTo(Temperature value)
        {
            return Kelvin.CompareTo(value.Kelvin);
        }

        /// <summary>
        /// Compares this instance to a specified object and returns an indication of
        /// their relative values.
        /// </summary>
        /// <param name="value">An object to compare, or null.</param>
        /// <returns>
        /// A signed number indicating the relative values of this instance and value.
        /// Value Description A negative integer This instance is less than value. Zero
        /// This instance is equal to value. A positive integer This instance is greater
        /// than value, or value is null.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// The value is not a Temperature.
        /// </exception>
        public int CompareTo(object value)
        {
            // TODO: Should the null check come after the type check?

            if (value == null) return 1;
            if (!(value is Temperature)) throw new ArgumentException();

            return CompareTo((Temperature)value);
        }

        /// <summary>
        /// Determines whether or not the given temperature is considered equal to this instance.
        /// </summary>
        /// <param name="value">The temperature to compare to this instance.</param>
        /// <returns>True if the temperature is considered equal to this instance. Otherwise, false.</returns>
        public bool Equals(Temperature value)
        {
            return Kelvin == value.Kelvin;
        }

        /// <summary>
        /// Determines whether or not the given temperature is considered equal to this instance
        /// within the given <paramref name="tolerance"/>. The <paramref name="unit"/> of temperature
        /// is used to evaluate the <paramref name="tolerance"/>.
        /// </summary>
        /// <param name="value">The temperature to compare to this instance.</param>
        /// <param name="tolerance">The deviation allowed for the temperatures to be considered equal.</param>
        /// <param name="unit">The unit of measurement that defines the <paramref name="tolerance"/>.</param>
        /// <returns>True if the temperatures are considered equal within the given tolerance. Otherwise, false.</returns>
        public bool Equals(Temperature value, double tolerance, TemperatureUnit unit)
        {
            return Math.Abs(ValueIn(unit) - value.ValueIn(unit)) <= tolerance;
        }

        /// <summary>
        /// Determines whether or not the given temperature is considered equal to this instance
        /// within the given <paramref name="tolerance"/>. Kelvin is used to evaluate the <paramref name="tolerance"/>.
        /// </summary>
        /// <param name="value">The temperature to compare to this instance.</param>
        /// <param name="tolerance">The deviation allowed for the temperatures to be considered equal.</param>
        /// <returns>True if the temperatures are considered equal within the given tolerance. Otherwise, false.</returns>
        public bool Equals(Temperature value, double tolerance)
        {
            return Equals(value, tolerance, TemperatureUnit.Kelvin);
        }

        /// <summary>
        /// Determines whether or not the given object is considered equal to the temperature.
        /// </summary>
        /// <param name="value">The object to compare to the temperature.</param>
        /// <returns>True if the object is considered equal to the temperature. Otherwise, false.</returns>
        public override bool Equals(object value)
        {
            if (value == null) return false;
            if (!(value is Temperature)) return false;

            return Equals((Temperature)value);
        }

        /// <summary>
        /// Finds the temperature change between the two temperatures using the specified <paramref name="unit"/>.
        /// </summary>
        /// <param name="value">The temperature to be subtracted from this temperature.</param>
        /// <param name="unit">The unit of measurement to use during the subtraction.</param>
        /// <returns>The change between the two temperatures, signed to indicate an increase or decrease in temperature.</returns>
        public double Change(Temperature value, TemperatureUnit unit)
        {
            return (ValueIn(unit) - value.ValueIn(unit)) * -1;
        }

        /// <summary>
        /// Finds the temperature change between the two temperatures using the Kelvin unit of measurement.
        /// </summary>
        /// <param name="value">The temperature to be subtracted from this temperature.</param>
        /// <returns>The change between the two temperatures, signed to indicate an increase or decrease in temperature.</returns>
        public double Change(Temperature value)
        {
            return Change(value, TemperatureUnit.Kelvin);
        }

        /// <summary>
        /// Returns the hash code for this instance.
        /// </summary>
        /// <returns>A 32-bit signed integer hash code.</returns>
        public override int GetHashCode()
        {
            return Kelvin.GetHashCode();
        }

        /// <summary>
        /// Determines the equality of two temperatures.
        /// </summary>
        /// <param name="t1">The first temperature to be compared.</param>
        /// <param name="t2">The second temperature to be compared.</param>
        /// <returns>True if the temperatures are equal. Otherwise, false.</returns>
        public static bool operator ==(Temperature t1, Temperature t2)
        {
            return t1.Kelvin == t2.Kelvin;
        }

        /// <summary>
        /// Determines the inequality of two temperatures.
        /// </summary>
        /// <param name="t1">The first temperature to be compared.</param>
        /// <param name="t2">The second temperature to be compared.</param>
        /// <returns>True if the temperatures are NOT equal. Otherwise, false.</returns>
        public static bool operator !=(Temperature t1, Temperature t2)
        {
            return t1.Kelvin != t2.Kelvin;
        }

        /// <summary>
        /// Determines whether one temperature is considered greater than another.
        /// </summary>
        /// <param name="t1">The first temperature to be compared.</param>
        /// <param name="t2">The second temperature to be compared.</param>
        /// <returns>True if the first temperature is greater than the second. Otherwise, false.</returns>
        public static bool operator >(Temperature t1, Temperature t2)
        {
            return t1.Kelvin > t2.Kelvin;
        }

        /// <summary>
        /// Determines whether one temperature is considered less than another.
        /// </summary>
        /// <param name="t1">The first temperature to be compared.</param>
        /// <param name="t2">The second temperature to be compared.</param>
        /// <returns>True if the first temperature is less than the second. Otherwise, false.</returns>
        public static bool operator <(Temperature t1, Temperature t2)
        {
            return t1.Kelvin < t2.Kelvin;
        }

        /// <summary>
        /// Determines whether one temperature is considered greater to or equal to another.
        /// </summary>
        /// <param name="t1">The first temperature to be compared.</param>
        /// <param name="t2">The second temperature to be compared.</param>
        /// <returns>
        /// True if the first temperature is greater to or equal to the second. Otherwise, false.
        /// </returns>
        public static bool operator >=(Temperature t1, Temperature t2)
        {
            return t1.Kelvin >= t2.Kelvin;
        }

        /// <summary>
        /// Determines whether one temperature is considered less than or equal to another.
        /// </summary>
        /// <param name="t1">The first temperature to be compared.</param>
        /// <param name="t2">The second temperature to be compared.</param>
        /// <returns>
        /// True if the first temperature is less than or equal to the second. Otherwise, false.
        /// </returns>
        public static bool operator <=(Temperature t1, Temperature t2)
        {
            return t1.Kelvin <= t2.Kelvin;
        }

        /// <summary>
        /// Converts a Kelvin temperature value to Celsius.
        /// </summary>
        /// <param name="kelvin">The Kelvin value to convert to Celsius.</param>
        /// <returns>The Kelvin value in Celsius.</returns>
        public static double KelvinToCelsius(double kelvin)
        {
            return kelvin - 273.15;
        }

        /// <summary>
        /// Converts a Celsius value to Kelvin.
        /// </summary>
        /// <param name="celsius">The Celsius value to convert to Kelvin.</param>
        /// <returns>The Celsius value in Kelvin.</returns>
        public static double CelsiusToKelvin(double celsius)
        {
            return celsius + 273.15;
        }

        /// <summary>
        /// Converts a Kelvin value to Fahrenheit.
        /// </summary>
        /// <param name="kelvin">The Kelvin value to convert to Fahrenheit.</param>
        /// <returns>The Kelvin value in Fahrenheit.</returns>
        public static double KelvinToFahrenheit(double kelvin)
        {
            return kelvin * 9 / 5 - 459.67;
        }

        /// <summary>
        /// Converts a Fahrenheit value to Kelvin.
        /// </summary>
        /// <param name="fahrenheit">The Fahrenheit value to convert to Kelvin.</param>
        /// <returns>The Fahrenheit value in Kelvin.</returns>
        public static double FahrenheitToKelvin(double fahrenheit)
        {
            return (fahrenheit + 459.67) * 5 / 9;
        }

        /// <summary>
        /// Converts a Fahrenheit value to Celsius.
        /// </summary>
        /// <param name="fahrenheit">The Fahrenheit value to convert to Celsius.</param>
        /// <returns>The Fahrenheit value in Celsius.</returns>
        public static double FahrenheitToCelsius(double fahrenheit)
        {
            return (fahrenheit - 32) * 5 / 9;
        }

        /// <summary>
        /// Converts a Celsius value to Fahrenheit.
        /// </summary>
        /// <param name="celsius">The Celsius value to convert to Fahrenheit.</param>
        /// <returns>The Celsius value in Fahrenheit.</returns>
        public static double CelsiusToFahrenheit(double celsius)
        {
            return celsius * 9 / 5 + 32;
        }
    }
}
